In [1]:
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import scipy.signal
In [2]:
x = np.fromfile('crewdragon2_symbols.f32', dtype = 'float32')[200:]
The figure below only shows a portion of the symbols.
In [3]:
plt.figure(figsize = (10,6), facecolor = 'w')
plt.plot(x[:10000], '.')
plt.title('Symbols')
plt.ylabel('Amplitude')
plt.xlabel('Symbol');
We correlate against the CCSDS ASM to find the start of frames.
In [4]:
sync = '1ACFFC1D'
sync_bits = 2*np.unpackbits(np.frombuffer(bytes.fromhex(sync), dtype = 'uint8')).astype('float32')-1
sync_corr = np.correlate(x, sync_bits)
In [5]:
plt.figure(figsize = (10,6), facecolor = 'w')
plt.plot(sync_corr[:40000]/sync_bits.size)
plt.title(f'Symbols correlation against 0x{sync} syncword')
plt.ylabel('Normalized correlation')
plt.xlabel('Symbol');
The CCSDS ASM occurs every 2000 bits.
In [6]:
np.average(np.diff(np.where(sync_corr/sync_bits.size > 0.7)[0]) == 2000)
Out[6]:
We align the frames manually.
In [7]:
y = x[1660:]
y = y[:y.size//2000*2000].reshape((-1,2000))
plt.plot(np.average(y, axis = 0))
Out[7]:
In [8]:
np.all(np.sign(np.average(y[:,:sync_bits.size], axis = 0)) == sync_bits)
Out[8]:
We plot the frames. We see that many frames have the same constant pattern.
In [9]:
frames = np.packbits((y[:,32:]>0).astype('uint8'), axis = 1)
plt.figure(figsize = (15,15), facecolor = 'w')
plt.imshow(frames, aspect = 0.1)
Out[9]:
To analyze those frames (which is full of "padding"), we take the first frame as an example.
We see that the pattern repeats every 255 bits and almost coincides with the CCSDS synchronous scrambler. This suggest that frames are scrambled and idle frames contain mainly zeros.
In [10]:
padding = y[0,32:]
padding_corr = np.correlate(padding, padding, mode = 'full')
plt.plot(padding_corr)
Out[10]:
In [11]:
np.diff(np.where(padding_corr > 100)[0])
Out[11]:
In [12]:
bytes(np.packbits((padding >0).astype('uint8'))).hex()
Out[12]:
In [13]:
frames = np.fromfile('crewdragon2_frames.u8', dtype = 'uint8').reshape((-1,214))
The decoded frames are shown below. We see a mixture of idle and non-idle frames.
In [14]:
plt.figure(figsize = (15,15), facecolor = 'w')
plt.imshow(frames, aspect = 0.005)
Out[14]:
Idle frames are marked by their first byte being 0xff
. We have an idle rate around 30%.
In [15]:
idle = frames[:,0] == 0xff
np.average(idle)
Out[15]:
In [16]:
N = 1000
plt.figure(figsize = (10,6), facecolor = 'w')
idle_avg = np.average(idle[:idle.size//N*N].reshape((-1,N)), axis = 1)
plt.plot(np.arange(idle_avg.size)*N, idle_avg)
plt.ylim((0,1))
plt.ylabel('Fraction of idle frames')
plt.xlabel('Frame')
plt.title('Link usage');
In non-idle frames the second byte contains a 7 bit frame counter.
In [17]:
plt.plot(frames[~idle,1][25000:][:1000])
Out[17]:
In [18]:
frame_loss = np.diff(frames[~idle,1]) % 128 - 1
plt.figure(figsize = (10,6), facecolor = 'w')
plt.plot(frame_loss)
plt.title('Frame loss')
plt.ylabel('Lost frames')
plt.xlabel('Frame');
According to the frame counter, only around 9% of the frames have been lost, however this shouldn't be trusted. For example, at the beginning of the recording no frames where decoded, and these don't enter this count.
In [19]:
np.average(frame_loss)
Out[19]:
We plot non-idle frames only. The data appears quite random.
In [20]:
plt.figure(figsize = (15,15), facecolor = 'w')
plt.imshow(frames[~idle], aspect = 0.003)
Out[20]: